////////////////////////////////////////////////
// HighCharts Plotting Code
////////////////////////////////////////////////

// Global plot data variable
mqc_plots = {};

// Initialise the toolbox filters
window.mqc_highlight_f_texts = [];
window.mqc_highlight_f_cols = [];
window.mqc_highlight_regex_mode = false;
window.mqc_rename_f_texts = [];
window.mqc_rename_t_texts = [];
window.mqc_rename_regex_mode = false;
window.mqc_hide_mode = "hide";
window.mqc_hide_f_texts = [];
window.mqc_hide_regex_mode = false;
window.HCDefaults = undefined;

// Execute when page load has finished loading
$(function () {
  // Show loading warning
  $(".mqc_loading_warning").show();

  // Decompress the JSON plot data
  mqc_plots = JSON.parse(LZString.decompressFromBase64(mqc_compressed_plotdata));

  // HighCharts Defaults
  window.HCDefaults = $.extend(true, {}, Highcharts.getOptions(), {});
  Highcharts.setOptions({
    credits: {
      enabled: true,
      text: "Created with MultiQC",
      href: "http://multiqc.info",
    },
    lang: {
      decimalPoint: mqc_config["decimalPoint_format"] == undefined ? "." : mqc_config["decimalPoint_format"],
      thousandsSep: mqc_config["thousandsSep_format"] == undefined ? " " : mqc_config["thousandsSep_format"],
    },
    exporting: {
      buttons: {
        contextButton: {
          menuItems: null,
          onclick: function () {
            // Tick only this plot in the toolbox and slide out
            $("#mqc_export_selectplots input").prop("checked", false);
            $('#mqc_export_selectplots input[value="' + this.renderTo.id + '"]').prop("checked", true);
            // Special case - Table scatter plots are in a modal, need to close this first
            if (this.renderTo.id === "tableScatterPlot") {
              $("#tableScatterModal").modal("hide");
            }
            mqc_toolbox_openclose("#mqc_exportplots", true);
          },
          text: '<span style="color:#999999;">Export Plot</span>',
          symbol:
            "url()",
          symbolX: 23,
          symbolY: 19,
        },
      },
    },
  });

  // Render plots on page load
  $(".hc-plot.not_rendered:visible:not(.gt_max_num_ds)").each(function () {
    var target = $(this).attr("id");
    // Only one point per dataset, so multiply limit by arbitrary number.
    var max_num = mqc_config["num_datasets_plot_limit"] * 50;
    // Deferring each plot call prevents browser from locking up
    setTimeout(function () {
      plot_graph(target, undefined, max_num);
      if ($(".hc-plot.not_rendered:visible:not(.gt_max_num_ds)").length == 0) {
        $(".mqc_loading_warning").hide();
      }
    }, 50);
  });
  if ($(".hc-plot.not_rendered:visible:not(.gt_max_num_ds)").length == 0) {
    $(".mqc_loading_warning").hide();
  }

  // Render a plot when clicked
  $("body").on("click", ".render_plot", function (e) {
    var target = $(this).parent().attr("id");
    plot_graph(target);
    if ($(".hc-plot.not_rendered").length == 0) {
      $("#mqc-warning-many-samples").hide();
    }
  });

  // Render all plots from header
  $("#mqc-render-all-plots").click(function () {
    $(".hc-plot.not_rendered").each(function () {
      var target = $(this).attr("id");
      plot_graph(target);
    });
    $("#mqc-warning-many-samples").hide();
  });

  // Replot graphs when something changed in filters
  $(document).on("mqc_highlights mqc_renamesamples mqc_hidesamples", function () {
    // Replot graphs
    $(".hc-plot:not(.not_rendered)").each(function () {
      var target = $(this).attr("id");
      plot_graph(target);
    });
  });

  // Switch a HighCharts axis or data source
  $(".hc_switch_group button").click(function (e) {
    e.preventDefault();
    $(this).siblings("button.active").removeClass("active");
    $(this).addClass("active");
    var target = $(this).data("target");
    var action = $(this).data("action");
    // Switch between values and percentages
    if (action == "set_percent" || action == "set_numbers") {
      var stack_type = action == "set_percent" ? "percent" : "normal";
      mqc_plots[target]["config"]["stacking"] = stack_type;
      mqc_plots[target]["config"]["ytype"] = "linear";
      // Workaround for manually set y-axis maximums on the % stacked plot
      if (action == "set_percent") {
        mqc_plots[target]["config"]["old_ymax"] = mqc_plots[target]["config"]["ymax"];
        delete mqc_plots[target]["config"]["ymax"];
      } else {
        if ("old_ymax" in mqc_plots[target]["config"]) {
          mqc_plots[target]["config"]["ymax"] = mqc_plots[target]["config"]["old_ymax"];
        }
      }
      plot_graph(target);
      var ylab = $(this).data("ylab");
      if (ylab != undefined) {
        $("#" + target)
          .highcharts()
          .yAxis[0].setTitle({ text: ylab });
      }
      var xlab = $(this).data("xlab");
      if (xlab != undefined) {
        $("#" + target)
          .highcharts()
          .xAxis[0].setTitle({ text: xlab });
      }
    }
    // Switch to log10 axis
    if (action == "set_log") {
      mqc_plots[target]["config"]["ytype"] = "logarithmic";
      plot_graph(target);
    }
    // Switch data source
    if (action == "set_data") {
      var ds = $(this).data("newdata");
      plot_graph(target, ds);
      var ylab = $(this).data("ylab");
      var xlab = $(this).data("xlab");
      var ymax = $(this).data("ymax");
      if (ylab != undefined) {
        $("#" + target)
          .highcharts()
          .yAxis[0].setTitle({ text: ylab });
      }
      if (xlab != undefined) {
        $("#" + target)
          .highcharts()
          .xAxis[0].setTitle({ text: xlab });
      }
      if (ymax != undefined) {
        $("#" + target)
          .highcharts()
          .yAxis[0].setExtremes(null, ymax);
      }
    }
  });

  // Make HighCharts divs height-draggable
  // http://jsfiddle.net/Lkwb86c8/
  $(".hc-plot:not(.no-handle)").each(function () {
    if (!$(this).parent().hasClass("hc-plot-wrapper")) {
      $(this).wrap('<div class="hc-plot-wrapper"></div>');
    }
    if (!$(this).siblings().hasClass("hc-plot-handle")) {
      $(this).after('<div class="hc-plot-handle"><span></span><span></span><span></span></div>');
    }
    $(this).css({ height: "auto", top: 0, bottom: "10px", position: "absolute" });
  });
  $(".hc-plot-handle").on("mousedown", function (e) {
    var wrapper = $(this).parent();
    var handle = $(this);
    var startHeight = wrapper.height();
    var pY = e.pageY;
    $(document).on("mouseup", function (e) {
      // Clear listeners now that we've let go
      $(document).off("mousemove");
      $(document).off("mouseup");
      // Fire off a custom jQuery event for other javascript chunks to tie into
      // Bind to the plot div, which should have a custom ID
      $(wrapper.parent().find(".hc-plot, .beeswarm-plot")).trigger("mqc_plotresize");
    });
    $(document).on("mousemove", function (me) {
      wrapper.css("height", startHeight + (me.pageY - pY));
    });
  });
  // Trigger HighCharts reflow when a plot is resized
  $(".hc-plot, .beeswarm-plot").on("mqc_plotresize", function (e) {
    if ($(this).highcharts()) {
      $(this).highcharts().reflow();
    }
  });

  // Switch a y axis limit on or off
  $(".mqc_hcplot_plotgroup").on("click", ".mqc_hcplot_yaxis_limit_toggle .mqc_switch_wrapper", function () {
    var target = $($(this).data("target")).highcharts();
    var ymax = $(this).data("ymax");
    var ymin = $(this).data("ymin");
    ymax = ymax == "undefined" ? null : ymax;
    ymin = ymin == "undefined" ? null : ymin;
    var mqc_switch = $(this).find(".mqc_switch");
    if (mqc_switch.hasClass("on")) {
      target.yAxis[0].update({ max: null, min: null });
      mqc_switch.removeClass("on").addClass("off").text("off");
    } else {
      target.yAxis[0].update({ max: ymax, min: ymin });
      mqc_switch.removeClass("off").addClass("on").text("on");
    }
  });

  // Sort a heatmap by highlighted names
  $(".mqc_heatmap_sortHighlight").click(function (e) {
    e.preventDefault();
    var target = $(this).data("target").substr(1);
    if (mqc_plots[target]["config"]["sortHighlights"] == true) {
      mqc_plots[target]["config"]["sortHighlights"] = false;
      $(this).removeClass("active");
    } else {
      mqc_plots[target]["config"]["sortHighlights"] = true;
      $(this).addClass("active");
    }
    $(this).blur();
    plot_heatmap(target);
  });
});

// Call to render any plot
function plot_graph(target, ds, max_num) {
  if (mqc_plots[target] === undefined) {
    return false;
  }

  // If no dataset specified, check if there is an active button and set automatically
  if (ds === undefined) {
    var ds_btns = $('.hc_switch_group button[data-action="set_data"][data-target="' + target + '"]');
    if (ds_btns.length) {
      ds = ds_btns.filter(".active").data("newdata");
    }
  }

  // If log status is not set and button is there, check whether it's active by default
  var config = mqc_plots[target]["config"];
  if (config === undefined) {
    mqc_plots[target]["config"] = {};
    config = {};
  }
  if (config["ytype"] === undefined) {
    var log_btn = $('.hc_switch_group button[data-action="set_log"][data-target="' + target + '"]');
    if (log_btn.length && log_btn.hasClass("active")) {
      config["ytype"] = "logarithmic";
    }
  }

  // XY Line charts
  if (mqc_plots[target]["plot_type"] == "xy_line") {
    if (max_num === undefined || mqc_plots[target]["datasets"][0].length < max_num) {
      plot_xy_line_graph(target, ds);
      $("#" + target).removeClass("not_rendered");
    } else {
      $("#" + target)
        .addClass("not_rendered gt_max_num_ds")
        .html('<button class="btn btn-default btn-lg render_plot">Show plot</button>');
    }
  }
  // Bar graphs
  else if (mqc_plots[target]["plot_type"] == "bar_graph") {
    if (max_num === undefined || mqc_plots[target]["samples"][0].length < max_num) {
      plot_stacked_bar_graph(target, ds);
      $("#" + target).removeClass("not_rendered");
    } else {
      $("#" + target)
        .addClass("not_rendered gt_max_num_ds")
        .html('<button class="btn btn-default btn-lg render_plot">Show plot</button>');
    }
  }
  // Scatter plots
  else if (mqc_plots[target]["plot_type"] == "scatter") {
    if (max_num === undefined || Object.keys(mqc_plots[target]["datasets"][0]).length < max_num) {
      plot_scatter_plot(target, ds);
      $("#" + target).removeClass("not_rendered");
    } else {
      $("#" + target)
        .addClass("not_rendered gt_max_num_ds")
        .html('<button class="btn btn-default btn-lg render_plot">Show plot</button>');
    }
  }
  // Beeswarm graphs
  else if (mqc_plots[target]["plot_type"] == "beeswarm") {
    if (max_num === undefined || mqc_plots[target]["samples"][0].length < max_num) {
      plot_beeswarm_graph(target, ds);
      $("#" + target).removeClass("not_rendered");
    } else {
      $("#" + target)
        .addClass("not_rendered gt_max_num_ds")
        .html('<button class="btn btn-default btn-lg render_plot">Show plot</button>');
    }
  }
  // Heatmap plots
  else if (mqc_plots[target]["plot_type"] == "heatmap") {
    if (max_num === undefined || mqc_plots[target]["xcats"][0].length < max_num) {
      plot_heatmap(target, ds);
      $("#" + target).removeClass("not_rendered");
    } else {
      $("#" + target)
        .addClass("not_rendered gt_max_num_ds")
        .html('<button class="btn btn-default btn-lg render_plot">Show plot</button>');
    }
  }
  // Not recognised
  else {
    console.log("Did not recognise plot type: " + mqc_plots[target]["plot_type"]);
  }
}

// Basic Line Graph
function plot_xy_line_graph(target, ds) {
  if (mqc_plots[target] === undefined || mqc_plots[target]["plot_type"] !== "xy_line") {
    return false;
  }
  var config = mqc_plots[target]["config"];
  var data = mqc_plots[target]["datasets"];
  if (ds === undefined) {
    ds = 0;
  }

  if (config["tt_label"] === undefined) {
    config["tt_label"] = "{point.x}: {point.y:.2f}";
    if (config["categories"]) {
      config["tt_formatter"] = function () {
        var yval =
          Highcharts.numberFormat(this.y, config["tt_decimals"] == undefined ? 0 : config["tt_decimals"]) +
          (config["tt_suffix"] || "");
        return (
          '<div style="background-color:' +
          this.series.color +
          '; display:inline-block; height: 10px; width: 10px; border:1px solid #333;"></div> <span style="text-decoration:underline; font-weight:bold;">' +
          this.series.name +
          "</span><br><strong>" +
          this.key +
          ":</strong> " +
          yval
        );
      };
    }
  }
  if (config["click_func"] === undefined) {
    config["click_func"] = function () {};
  } else {
    config["click_func"] = eval("(" + config["click_func"] + ")");
    if (config["cursor"] === undefined) {
      config["cursor"] = "pointer";
    }
  }
  if (config["xDecimals"] === undefined) {
    config["xDecimals"] = true;
  }
  if (config["yDecimals"] === undefined) {
    config["yDecimals"] = true;
  }
  if (config["pointFormat"] === undefined) {
    config["pointFormat"] =
      '<div style="background-color:{series.color}; display:inline-block; height: 10px; width: 10px; border:1px solid #333;"></div> <span style="text-decoration:underline; font-weight:bold;">{series.name}</span><br>' +
      config["tt_label"];
  }
  if (config["ytype"] === undefined) {
    config["ytype"] = config["yLog"] ? "logarithmic" : "linear";
  }
  if (config["ytype"] == "logarithmic") {
    if (config["ymin"] == 0) {
      config["ymin"] = undefined;
    }
    var minTickInt = "auto";
  } else {
    var minTickInt = undefined;
  }

  // Make a clone of the data, so that we can mess with it,
  // while keeping the original data in tact
  var data = JSON.parse(JSON.stringify(mqc_plots[target]["datasets"][ds]));

  // Rename samples
  if (window.mqc_rename_f_texts.length > 0) {
    $.each(data, function (j, s) {
      $.each(window.mqc_rename_f_texts, function (idx, f_text) {
        if (window.mqc_rename_regex_mode) {
          var re = new RegExp(f_text, "g");
          data[j]["name"] = data[j]["name"].replace(re, window.mqc_rename_t_texts[idx]);
        } else {
          data[j]["name"] = data[j]["name"].replace(f_text, window.mqc_rename_t_texts[idx]);
        }
      });
    });
  }

  // Highlight samples
  if (window.mqc_highlight_f_texts.length > 0) {
    $.each(data, function (j, s) {
      $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
        if (
          (window.mqc_highlight_regex_mode && data[j]["name"].match(f_text)) ||
          (!window.mqc_highlight_regex_mode && data[j]["name"].indexOf(f_text) > -1)
        ) {
          data[j]["color"] = window.mqc_highlight_f_cols[idx];
        }
      });
    });
  }

  // Hide samples
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .parent()
    .find(".samples-hidden-warning")
    .remove();
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .show();
  if (window.mqc_hide_f_texts.length > 0) {
    var num_hidden = 0;
    var num_total = data.length;
    var j = data.length;
    while (j--) {
      var match = false;
      for (i = 0; i < window.mqc_hide_f_texts.length; i++) {
        var f_text = window.mqc_hide_f_texts[i];
        if (window.mqc_hide_regex_mode) {
          if (data[j]["name"].match(f_text)) {
            match = true;
          }
        } else {
          if (data[j]["name"].indexOf(f_text) > -1) {
            match = true;
          }
        }
      }
      if (window.mqc_hide_mode == "show") {
        match = !match;
      }
      if (match) {
        data.splice(j, 1);
        num_hidden += 1;
      }
    }
    // Some series hidden. Show a warning text string.
    if (num_hidden > 0) {
      var alert =
        '<div class="samples-hidden-warning alert alert-warning"><span class="glyphicon glyphicon-info-sign"></span> <strong>Warning:</strong> ' +
        num_hidden +
        ' samples hidden. <a href="#mqc_hidesamples" class="alert-link" onclick="mqc_toolbox_openclose(\'#mqc_hidesamples\', true); return false;">See toolbox.</a></div>';
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .before(alert);
    }
    // All series hidden. Hide the graph.
    if (num_hidden == num_total) {
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .hide();
      return false;
    }
  }

  // Make the highcharts plot
  Highcharts.chart(target, {
    chart: {
      type: "line",
      zoomType: "x",
    },
    title: {
      text: config["title"],
      x: 30, // fudge to center over plot area rather than whole plot
    },
    xAxis: {
      title: {
        text: config["xlab"],
      },
      labels: { format: config["xLabelFormat"] ? config["xLabelFormat"] : "{value}" },
      type: config["xLog"] ? "logarithmic" : "linear",
      categories: config["categories"],
      ceiling: config["xCeiling"],
      floor: config["xFloor"],
      max: config["xmax"],
      min: config["xmin"],
      minRange: config["xMinRange"],
      allowDecimals: config["xDecimals"],
      plotBands: config["xPlotBands"],
      plotLines: config["xPlotLines"],
    },
    yAxis: {
      title: {
        text: config["ylab"],
      },
      labels: { format: config["yLabelFormat"] ? config["yLabelFormat"] : "{value}" },
      type: config["ytype"],
      ceiling: config["yCeiling"],
      floor: config["yFloor"],
      max: config["ymax"],
      min: config["ymin"],
      minRange: config["yMinRange"],
      allowDecimals: config["yDecimals"],
      plotBands: config["yPlotBands"],
      plotLines: config["yPlotLines"],
    },
    plotOptions: {
      series: {
        marker: { enabled: false },
        cursor: config["cursor"],
        point: {
          events: {
            click: config["click_func"],
          },
        },
      },
    },
    legend: {
      enabled: false,
    },
    tooltip: {
      headerFormat: "",
      pointFormat: config["pointFormat"],
      formatter: config["tt_formatter"],
      useHTML: true,
    },
    series: data,
  });
}

// Stacked Bar Graph
function plot_stacked_bar_graph(target, ds) {
  if (mqc_plots[target] === undefined || mqc_plots[target]["plot_type"] !== "bar_graph") {
    return false;
  }
  if (ds === undefined) {
    ds = 0;
  }

  // Make a clone of the everything, so that we can mess with it,
  // while keeping the original data in tact
  var data = JSON.parse(JSON.stringify(mqc_plots[target]["datasets"][ds]));
  var cats = JSON.parse(JSON.stringify(mqc_plots[target]["samples"][ds]));
  var config = JSON.parse(JSON.stringify(mqc_plots[target]["config"]));

  if (config["stacking"] === undefined) {
    config["stacking"] = "normal";
  }
  if (config["stacking"] === "normal") {
    config["groupPadding"] = "0.02";
    config["pointPadding"] = "0.1";
  } else {
    config["groupPadding"] = "0.1";
    config["pointPadding"] = 0;
  }
  if (config["ytype"] === undefined) {
    config["ytype"] = "linear";
  }
  if (config["reversedStacks"] === undefined) {
    config["reversedStacks"] = false;
  }
  if (config["use_legend"] === undefined) {
    config["use_legend"] = true;
  }
  if (config["yDecimals"] === undefined) {
    config["yDecimals"] = true;
  }
  if (config["click_func"] === undefined) {
    config["click_func"] = function () {};
  } else {
    if (config["cursor"] === undefined) {
      config["cursor"] = "pointer";
    }
  }
  if (config["tt_percentages"] === undefined) {
    config["tt_percentages"] = true;
  }
  if (config["borderWidth"] === undefined) {
    config["borderWidth"] = 0;
  }

  if (config["ytype"] == "logarithmic") {
    if (config["ymin"] == 0 || config["ymin"] == undefined) {
      config["ymin"] = 1;
    }
    var minTickInt = "auto";
  } else {
    var minTickInt = undefined;
  }

  // Rename samples
  if (window.mqc_rename_f_texts.length > 0) {
    $.each(cats, function (j, s_name) {
      $.each(window.mqc_rename_f_texts, function (idx, f_text) {
        if (window.mqc_rename_regex_mode) {
          var re = new RegExp(f_text, "g");
          cats[j] = cats[j].replace(re, window.mqc_rename_t_texts[idx]);
        } else {
          cats[j] = cats[j].replace(f_text, window.mqc_rename_t_texts[idx]);
        }
      });
    });
  }

  // Highlight samples
  if (window.mqc_highlight_f_texts.length > 0) {
    $.each(cats, function (j, s_name) {
      $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
        if (f_text == "") {
          return true;
        } // skip blanks
        if (
          (window.mqc_highlight_regex_mode && s_name.match(f_text)) ||
          (!window.mqc_highlight_regex_mode && s_name.indexOf(f_text) > -1)
        ) {
          // Make the data point in each series with this index have a border colour
          $.each(data, function (k, d) {
            data[k]["data"][j] = {
              y: data[k]["data"][j],
              borderColor: window.mqc_highlight_f_cols[idx],
            };
          });
        }
      });
    });
    // Bump the borderWidth to make the highlights more obvious
    if (config["borderWidth"] <= 2) {
      config["borderWidth"] = 2;
    }
  }

  // Hide samples
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .parent()
    .find(".samples-hidden-warning")
    .remove();
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .show();
  if (window.mqc_hide_f_texts.length > 0) {
    var num_hidden = 0;
    var num_total = cats.length;
    var j = cats.length;
    while (j--) {
      var s_name = cats[j];
      var match = false;
      for (i = 0; i < window.mqc_hide_f_texts.length; i++) {
        var f_text = window.mqc_hide_f_texts[i];
        if (window.mqc_hide_regex_mode) {
          if (s_name.match(f_text)) {
            match = true;
          }
        } else {
          if (s_name.indexOf(f_text) > -1) {
            match = true;
          }
        }
      }
      if (window.mqc_hide_mode == "show") {
        match = !match;
      }
      if (match) {
        cats.splice(j, 1);
        $.each(data, function (k, d) {
          data[k]["data"].splice(j, 1);
        });
        num_hidden += 1;
      }
    }
    // Some series hidden. Show a warning text string.
    if (num_hidden > 0) {
      var alert =
        '<div class="samples-hidden-warning alert alert-warning"><span class="glyphicon glyphicon-info-sign"></span> <strong>Warning:</strong> ' +
        num_hidden +
        ' samples hidden. <a href="#mqc_hidesamples" class="alert-link" onclick="mqc_toolbox_openclose(\'#mqc_hidesamples\', true); return false;">See toolbox.</a></div>';
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .before(alert);
    }
    // All series hidden. Hide the graph.
    if (num_hidden == num_total) {
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .hide();
      return false;
    }
  }

  // Make the highcharts plot
  Highcharts.chart(target, {
    chart: {
      type: "bar",
      zoomType: "x",
    },
    title: {
      text: config["title"],
    },
    xAxis: {
      categories: cats,
      min: 0,
      title: {
        text: config["xlab"],
      },
    },
    yAxis: {
      title: {
        text: config["ylab"],
      },
      labels: { format: config["yLabelFormat"] ? config["yLabelFormat"] : "{value}" },
      ceiling: config["yCeiling"],
      floor: config["yFloor"],
      minRange: config["yMinRange"],
      max: config["ymax"],
      min: config["ymin"],
      type: config["ytype"],
      labels: {
        format: config["ylab_format"],
      },
      allowDecimals: config["yDecimals"],
      reversedStacks: config["reversedStacks"],
      minorTickInterval: minTickInt,
    },
    plotOptions: {
      series: {
        stacking: config["stacking"],
        pointPadding: config["pointPadding"],
        groupPadding: config["groupPadding"],
        borderWidth: config["borderWidth"],
      },
      cursor: config["cursor"],
      point: {
        events: {
          click: config["click_func"],
        },
      },
    },
    legend: {
      enabled: config["use_legend"],
    },
    tooltip: {
      formatter: function () {
        var colspan = config["tt_percentages"] ? 3 : 2;
        var s =
          '<table><tr><th colspan="' +
          colspan +
          '" style="font-weight:bold; text-decoration:underline;">' +
          this.x +
          "</th></tr>";
        $.each(this.points, function () {
          yval =
            Highcharts.numberFormat(this.y, config["tt_decimals"] == undefined ? 0 : config["tt_decimals"]) +
            (config["tt_suffix"] || "");
          ypct = Highcharts.numberFormat(this.percentage, 1);
          s +=
            '<tr> \
            <td style="font-weight:bold; color:' +
            this.series.color +
            '; border-bottom:1px solid #dedede;">' +
            this.series.name +
            ':</td>\
            <td style="text-align:right; border-bottom:1px solid #dedede; padding: 0 15px;">' +
            yval +
            "</td>";
          if (config["tt_percentages"]) {
            s += '<td style="text-align:right; border-bottom:1px solid #dedede;">(' + ypct + "%)</td>";
          }
          s += "</tr>";
        });
        s += "</table>";
        return s;
      },
      shared: true,
      useHTML: true,
    },
    series: data,
  });
}

// Scatter plot
function plot_scatter_plot(target, ds) {
  if (mqc_plots[target] === undefined || mqc_plots[target]["plot_type"] !== "scatter") {
    return false;
  }
  var config = mqc_plots[target]["config"];
  var data = mqc_plots[target]["datasets"];
  if (ds === undefined) {
    ds = 0;
  }

  if (config["marker_colour"] === undefined) {
    config["marker_colour"] = "rgba(124, 181, 236, .5)";
  }
  if (config["marker_size"] === undefined) {
    config["marker_size"] = 5;
  }
  if (config["marker_line_colour"] === undefined) {
    config["marker_line_colour"] = "#999";
  }
  if (config["marker_line_width"] === undefined) {
    config["marker_line_width"] = 1;
  }
  if (config["tt_label"] === undefined) {
    config["tt_label"] = "X: <strong>{point.x:.2f}</strong><br/>Y: <strong>{point.y:.2f}</strong>";
  }
  if (config["click_func"] === undefined) {
    config["click_func"] = function () {};
  } else {
    config["click_func"] = eval("(" + config["click_func"] + ")");
    if (config["cursor"] === undefined) {
      config["cursor"] = "pointer";
    }
  }
  if (config["xDecimals"] === undefined) {
    config["xDecimals"] = true;
  }
  if (config["yDecimals"] === undefined) {
    config["yDecimals"] = true;
  }
  if (config["pointFormat"] === undefined) {
    config["pointFormat"] =
      '<div style="background-color:{point.color}; display:inline-block; height: 10px; width: 10px; border:1px solid #333;"></div> <span style="text-decoration:underline; font-weight:bold;">{point.name}</span><br>' +
      config["tt_label"];
  }

  // Make a clone of the data, so that we can mess with it,
  // while keeping the original data in tact
  var data = JSON.parse(JSON.stringify(mqc_plots[target]["datasets"][ds]));

  $.each(data, function (j, s) {
    data[j]["marker"] = {
      radius: data[j]["marker_size"] ?? config["marker_size"],
      lineWidth: data[j]["marker_line_width"] ?? config["marker_line_width"],
    };
  });

  // Rename samples
  if (window.mqc_rename_f_texts.length > 0) {
    $.each(data, function (j, s) {
      $.each(window.mqc_rename_f_texts, function (idx, f_text) {
        if (window.mqc_rename_regex_mode) {
          var re = new RegExp(f_text, "g");
          data[j]["name"] = data[j]["name"].replace(re, window.mqc_rename_t_texts[idx]);
        } else {
          data[j]["name"] = data[j]["name"].replace(f_text, window.mqc_rename_t_texts[idx]);
        }
      });
    });
  }

  // Highlight samples
  if (window.mqc_highlight_f_texts.length > 0) {
    $.each(data, function (j, s) {
      if ("marker" in data[j]) {
        data[j]["marker"]["lineWidth"] = 0;
      } else {
        data[j]["marker"] = { lineWidth: 0 };
      }
      var match = false;
      $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
        if (f_text == "") {
          return true;
        }
        if (
          (window.mqc_highlight_regex_mode && data[j]["name"].match(f_text)) ||
          (!window.mqc_highlight_regex_mode && data[j]["name"].indexOf(f_text) > -1)
        ) {
          data[j]["color"] = window.mqc_highlight_f_cols[idx];
          match = true;
        }
      });
      if (!match) {
        data[j]["color"] = "rgba(100,100,100,0.2)";
      }
    });
  }

  // Hide samples
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .parent()
    .find(".samples-hidden-warning")
    .remove();
  $("#" + target)
    .closest(".mqc_hcplot_plotgroup")
    .show();
  if (window.mqc_hide_f_texts.length > 0) {
    var num_hidden = 0;
    var num_total = data.length;
    var j = data.length;
    while (j--) {
      var match = false;
      for (i = 0; i < window.mqc_hide_f_texts.length; i++) {
        var f_text = window.mqc_hide_f_texts[i];
        if (window.mqc_hide_regex_mode) {
          if (data[j]["name"].match(f_text)) {
            match = true;
          }
        } else {
          if (data[j]["name"].indexOf(f_text) > -1) {
            match = true;
          }
        }
      }
      if (window.mqc_hide_mode == "show") {
        match = !match;
      }
      if (match) {
        data.splice(j, 1);
        num_hidden += 1;
      }
    }
    // Some series hidden. Show a warning text string.
    if (num_hidden > 0) {
      var alert =
        '<div class="samples-hidden-warning alert alert-warning"><span class="glyphicon glyphicon-info-sign"></span> <strong>Warning:</strong> ' +
        num_hidden +
        ' samples hidden. <a href="#mqc_hidesamples" class="alert-link" onclick="mqc_toolbox_openclose(\'#mqc_hidesamples\', true); return false;">See toolbox.</a></div>';
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .before(alert);
    }
    // All series hidden. Hide the graph.
    if (num_hidden == num_total) {
      $("#" + target)
        .closest(".mqc_hcplot_plotgroup")
        .hide();
      return false;
    }
  }

  // Make the highcharts plot
  Highcharts.chart(
    target,
    {
      chart: {
        type: "scatter",
        zoomType: "xy",
        plotBorderWidth: 1,
        height: config["square"] ? 500 : undefined,
        width: config["square"] ? 500 : undefined,
      },
      title: {
        text: config["title"],
        x: 30, // fudge to center over plot area rather than whole plot
      },
      xAxis: {
        title: {
          text: config["xlab"],
        },
        type: config["xLog"] ? "logarithmic" : "linear",
        gridLineWidth: 1,
        categories: config["categories"],
        ceiling: config["xCeiling"],
        floor: config["xFloor"],
        max: config["xmax"],
        min: config["xmin"],
        minRange: config["xMinRange"],
        allowDecimals: config["xDecimals"],
        plotBands: config["xPlotBands"],
        plotLines: config["xPlotLines"],
        endOnTick: config["endOnTick"],
      },
      yAxis: {
        title: {
          text: config["ylab"],
        },
        type: config["yLog"] ? "logarithmic" : "linear",
        ceiling: config["yCeiling"],
        floor: config["yFloor"],
        max: config["ymax"],
        min: config["ymin"],
        minRange: config["yMinRange"],
        allowDecimals: config["yDecimals"],
        plotBands: config["yPlotBands"],
        plotLines: config["yPlotLines"],
        endOnTick: config["endOnTick"],
      },
      plotOptions: {
        series: {
          animation: false,
          marker: {
            radius: config["marker_size"],
            lineColor: config["marker_line_colour"],
            lineWidth: config["marker_line_width"],
            states: {
              hover: {
                enabled: config["enableHover"] == undefined ? true : config["enableHover"],
                lineColor: "rgb(100,100,100)",
              },
            },
          },
          turboThreshold: config["turboThreshold"],
          enableMouseTracking: config["enableMouseTracking"],
          cursor: config["cursor"],
          point: {
            events: {
              click: config["click_func"],
            },
          },
        },
      },
      legend: {
        enabled: false,
      },
      tooltip: {
        headerFormat: "",
        pointFormat: config["pointFormat"],
        useHTML: true,
        formatter: function () {
          if (!this.point.noTooltip) {
            // Formatter function doesn't do name for some reason
            fstring = config["pointFormat"].replace("{point.name}", this.point.name);
            return Highcharts.Point.prototype.tooltipFormatter.call(this, fstring);
          }
          return false;
        },
      },
      series: [
        {
          color: config["marker_colour"],
          data: data,
        },
      ],
    },
    // Maintain aspect ratio as chart size changes
    function (this_chart) {
      if (config["square"]) {
        var resizeCh = function (chart) {
          // Extra width for legend
          var lWidth = chart.options.legend.enabled ? 30 : 0;
          // Work out new chart width, assuming needs to be narrower
          var chHeight = $(chart.renderTo).height();
          var chWidth = $(chart.renderTo).width();
          var nChHeight = chHeight;
          var nChWidth = chHeight + lWidth;
          // Chart is already too narrow, make it less tall
          if (chWidth < nChWidth) {
            nChHeight = chWidth - lWidth;
            nChWidth = chWidth;
          }
          chart.setSize(nChWidth, nChHeight);
        };
        // Resize on load
        resizeCh(this_chart);
        // Resize on graph resize
        $(this_chart.renderTo).on("mqc_plotresize", function (e) {
          resizeCh(this_chart);
        });
      }
    },
  );
}

// Beeswarm plot
function plot_beeswarm_graph(target, ds) {
  if (mqc_plots[target] === undefined || mqc_plots[target]["plot_type"] !== "beeswarm") {
    return false;
  }
  var config = mqc_plots[target]["config"];
  if (ds === undefined) {
    ds = 0;
  }

  // Make a clone of the data, so that we can mess with it,
  // while keeping the original data in tact
  var datasets = JSON.parse(JSON.stringify(mqc_plots[target]["datasets"]));
  var samples = JSON.parse(JSON.stringify(mqc_plots[target]["samples"]));
  var categories = JSON.parse(JSON.stringify(mqc_plots[target]["categories"]));

  // Rename samples
  if (window.mqc_rename_f_texts.length > 0) {
    for (i = 0; i < samples.length; i++) {
      for (j = 0; j < samples[i].length; j++) {
        $.each(window.mqc_rename_f_texts, function (idx, f_text) {
          if (window.mqc_rename_regex_mode) {
            var re = new RegExp(f_text, "g");
            samples[i][j] = samples[i][j].replace(re, window.mqc_rename_t_texts[idx]);
          } else {
            samples[i][j] = samples[i][j].replace(f_text, window.mqc_rename_t_texts[idx]);
          }
        });
      }
    }
  }

  // Highlight samples
  var baseColour = "rgb(55,126,184)"; // Blue points by default
  var seriesColours = {};
  if (window.mqc_highlight_f_texts.length > 0) {
    baseColour = "rgb(80,80,80)"; // Grey points if no highlight
    for (i = 0; i < samples.length; i++) {
      for (j = 0; j < samples[i].length; j++) {
        $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
          if (
            (window.mqc_highlight_regex_mode && samples[i][j].match(f_text)) ||
            (!window.mqc_highlight_regex_mode && samples[i][j].indexOf(f_text) > -1)
          ) {
            seriesColours[samples[i][j]] = window.mqc_highlight_f_cols[idx];
          }
        });
      }
    }
  }

  // Hide samples
  $("#" + target)
    .closest(".hc-plot-wrapper")
    .parent()
    .find(".samples-hidden-warning")
    .remove();
  $("#" + target)
    .closest(".hc-plot-wrapper")
    .show();
  if (window.mqc_hide_f_texts.length > 0) {
    var num_hidden = 0;
    var num_total = 0;
    for (i = 0; i < samples.length; i++) {
      num_total = Math.max(num_total, samples[i].length);
      var j = samples[i].length;
      var hidden_here = 0;
      while (j--) {
        var s_name = samples[i][j];
        var match = false;
        for (k = 0; k < window.mqc_hide_f_texts.length; k++) {
          var f_text = window.mqc_hide_f_texts[k];
          if (window.mqc_hide_regex_mode) {
            if (s_name.match(f_text)) {
              match = true;
            }
          } else {
            if (s_name.indexOf(f_text) > -1) {
              match = true;
            }
          }
        }
        if (window.mqc_hide_mode == "show") {
          match = !match;
        }
        if (match) {
          samples[i].splice(j, 1);
          datasets[i].splice(j, 1);
          hidden_here += 1;
        }
      }
      num_hidden = Math.max(num_hidden, hidden_here);
    }
    // Some series hidden. Show a warning text string.
    if (num_hidden > 0) {
      var alert =
        '<div class="samples-hidden-warning alert alert-warning"><span class="glyphicon glyphicon-info-sign"></span> <strong>Warning:</strong> ' +
        num_hidden +
        ' samples hidden. <a href="#mqc_hidesamples" class="alert-link" onclick="mqc_toolbox_openclose(\'#mqc_hidesamples\', true); return false;">See toolbox.</a></div>';
      $("#" + target)
        .closest(".hc-plot-wrapper")
        .before(alert);
    }
    // All series hidden. Hide the graph.
    if (num_hidden == num_total) {
      $("#" + target)
        .closest(".hc-plot-wrapper")
        .hide();
      return false;
    }
  }

  // Figure out how tall to make each plot
  var ph_min = 40;
  var ph_max = 100;
  var pheight = 600 / categories.length;
  pheight = Math.min(ph_max, Math.max(ph_min, pheight));

  // Clear the loading text and add hover text placeholder
  $("#" + target).html(
    '<div class="beeswarm-hovertext"><em class="placeholder">Hover over a data point for more information</em></div><div class="beeswarm-plots"></div>',
  );
  // Resize the parent draggable div
  $("#" + target)
    .parent()
    .css("height", pheight * categories.length + 40 + "px");

  for (var i = 0; i < categories.length; i++) {
    var borderCol = categories[i]["bordercol"];
    if (borderCol == undefined) {
      borderCol = "#cccccc";
    }

    var data = datasets[i];
    var s_names = samples[i];
    if (categories[i]["namespace"] == "") {
      var label = categories[i]["title"];
      var label_long = categories[i]["description"];
    } else {
      var label = categories[i]["namespace"] + "<br/>" + categories[i]["title"];
      var label_long = categories[i]["namespace"] + ": " + categories[i]["description"];
    }
    var ttSuffix = categories[i]["suffix"];
    var decimalPlaces = categories[i]["decimalPlaces"];
    var minx = categories[i]["min"];
    var maxx = categories[i]["max"];

    // Size and spacing options
    var markerRadius = 2.5;
    var yspace = 70;
    var ysep = 10;
    if (data.length > 50) {
      markerRadius = 1.8;
      yspace = 50;
      ysep = 20;
    }
    if (data.length > 200) {
      markerRadius = 1;
      yspace = 30;
      ysep = 30;
    }

    if (maxx == undefined) {
      maxx = Math.max.apply(null, data);
    }
    if (minx == undefined) {
      minx = Math.max.apply(null, data);
    }
    var range = maxx - minx;
    var sep = range / yspace;
    // Get an array of indexes from a sorted data array
    // Leaves the data order in tact so we don't lose s_name association
    var indices = new Array(data.length);
    for (var n = 0; n < data.length; n++) {
      indices[n] = n;
    }
    indices.sort(function (a, b) {
      return data[a] < data[b] ? -1 : data[a] > data[b] ? 1 : 0;
    });
    var xydata = [];
    var last = undefined;
    var side = 1;
    for (var s_idx = 0; s_idx < indices.length; s_idx++) {
      row = indices[s_idx];
      s_name = s_names[row];
      d = data[row];
      if (Math.floor(d / sep) !== last) {
        last = Math.floor(d / sep);
        side = 1;
      } else {
        side += 1;
      }
      multiplier = side % 2 == 0 ? 1 : -1;
      var y = (Math.floor(side / 2) * multiplier) / ysep;
      // Don't let jitter get too big
      while (y > 1 || y < -1) {
        var n = Math.floor(Math.abs(y)) + 1;
        y = (Math.floor(side / 2) * multiplier) / (ysep * n);
      }
      // Get the point colour
      var thisCol = baseColour;
      if (s_name in seriesColours) {
        thisCol = seriesColours[s_name];
      }
      xydata.push({
        x: d,
        y: y,
        name: s_name,
        color: thisCol,
      });
    }

    $('<div class="beeswarm-plot" />')
      .appendTo("#" + target + " .beeswarm-plots")
      .css({
        "border-left": "2px solid " + borderCol,
        height: 100 / categories.length + "%",
      })
      .highcharts({
        chart: {
          type: "scatter",
          spacingTop: 0,
          marginBottom: 0,
          marginRight: 20,
          marginLeft: 180,
          backgroundColor: "transparent",
          // Horrible hacky HighCharts reflow problem.
          // TODO: Come back and find a better solution!
          events: {
            load: function (chart) {
              setTimeout(function () {
                chart.target.reflow();
              }, 200);
            },
          },
        },
        title: {
          text: label,
          align: "left",
          verticalAlign: "middle",
          y: 10,
          useHTML: true,
          style: {
            fontSize: "12px",
          },
        },
        yAxis: {
          title: { text: null },
          max: 1,
          min: -1,
          gridLineWidth: 0,
          title: { text: null },
          labels: { enabled: false },
          lineWidth: 0,
        },
        xAxis: {
          lineWidth: 0,
          tickWidth: 0,
          tickPixelInterval: 200,
          labels: {
            reserveSpace: false,
            y: -1 * (pheight / 2) + 5,
            zIndex: 1,
            style: {
              color: "#999999",
            },
          },
          min: minx,
          max: maxx,
        },
        tooltip: {
          valueSuffix: ttSuffix,
          valueDecimals: decimalPlaces,
          formatter: function () {
            var value = Highcharts.numberFormat(this.point.x, this.series.tooltipOptions.valueDecimals);
            var suff = this.series.tooltipOptions.valueSuffix;
            var ttstring =
              '<span style="float:right;">' +
              this.series.name +
              "</span><samp>" +
              this.point.name +
              "</samp>: &nbsp; <strong>" +
              value +
              " " +
              suff +
              "</strong>";
            $("#" + target + " .beeswarm-hovertext").html(ttstring);
            return false;
          },
        },
        plotOptions: {
          series: {
            name: label_long,
            turboThreshold: 0,
            marker: {
              radius: markerRadius,
              states: {
                hover: {
                  radiusPlus: 4,
                  lineWidthPlus: 2,
                  lineColor: "#333333",
                },
              },
            },
            stickyTracking: false,
            point: {
              events: {
                mouseOver: function (e) {
                  var hovName = this.name;
                  $("#" + target + " .beeswarm-plot").each(function () {
                    var plot = $(this).highcharts();
                    for (i = 0; i < plot.series[0].data.length; ++i) {
                      if (plot.series[0].data[i].name == hovName) {
                        plot.series[0].data[i].setState("hover");
                      }
                    }
                  });
                },
                mouseOut: function () {
                  $("#" + target + " .beeswarm-plot").each(function () {
                    var plot = $(this).highcharts();
                    for (i = 0; i < plot.series[0].data.length; ++i) {
                      plot.series[0].data[i].setState();
                    }
                  });
                  $("#" + target + " .beeswarm-hovertext").html(
                    '<em class="placeholder">Hover over a data point for more information</em>',
                  );
                },
              },
            },
          },
        },
        legend: { enabled: false },
        credits: { enabled: false },
        exporting: { enabled: false },
        series: [
          {
            data: xydata,
            // Workaround for HighCharts bug. See https://github.com/highcharts/highcharts/issues/1440
            marker: { states: { hover: { fillColor: {} } } },
          },
        ],
      });
  }
}

// Heatmap plot
function plot_heatmap(target, ds) {
  if (mqc_plots[target] === undefined || mqc_plots[target]["plot_type"] !== "heatmap") {
    return false;
  }
  var config = mqc_plots[target]["config"];

  if (config["square"] === undefined) {
    config["square"] = true;
  }
  if (config["xcats_samples"] === undefined) {
    config["xcats_samples"] = true;
  }
  if (config["ycats_samples"] === undefined) {
    config["ycats_samples"] = true;
  }

  // Make a clone of the data, so that we can mess with it,
  // while keeping the original data in tact
  var data = JSON.parse(JSON.stringify(mqc_plots[target]["data"]));
  var xcats = JSON.parse(JSON.stringify(mqc_plots[target]["xcats"]));
  var ycats = JSON.parse(JSON.stringify(mqc_plots[target]["ycats"]));
  // "xcats" and "ycats" are labels of columns and rows respectively
  // data[n] has form of [x,y,value], x/y are indices of columns/rows

  // Rename samples
  if (window.mqc_rename_f_texts.length > 0) {
    if (config["xcats_samples"]) {
      for (i = 0; i < xcats.length; i++) {
        $.each(window.mqc_rename_f_texts, function (idx, f_text) {
          if (window.mqc_rename_regex_mode) {
            var re = new RegExp(f_text, "g");
            xcats[i] = xcats[i].replace(re, window.mqc_rename_t_texts[idx]);
          } else {
            xcats[i] = xcats[i].replace(f_text, window.mqc_rename_t_texts[idx]);
          }
        });
      }
    }
    if (config["ycats_samples"]) {
      for (i = 0; i < ycats.length; i++) {
        $.each(window.mqc_rename_f_texts, function (idx, f_text) {
          if (window.mqc_rename_regex_mode) {
            var re = new RegExp(f_text, "g");
            ycats[i] = ycats[i].replace(re, window.mqc_rename_t_texts[idx]);
          } else {
            ycats[i] = ycats[i].replace(f_text, window.mqc_rename_t_texts[idx]);
          }
        });
      }
    }
  }

  // Sort samples by highlight
  $(".mqc_heatmap_sortHighlight").attr("disabled", false);
  if (config["sortHighlights"] == true) {
    if (window.mqc_highlight_f_texts.length > 0) {
      // Collect the highlighting indices
      var xcat_hl = Array();
      var ycat_hl = Array();
      for (i = 0; i < xcats.length; i++) {
        $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
          if (f_text == "") {
            xcat_hl[i] = 0;
          } else if (
            (window.mqc_highlight_regex_mode && xcats[i].match(f_text)) ||
            (!window.mqc_highlight_regex_mode && xcats[i].indexOf(f_text) > -1)
          ) {
            xcat_hl[i] = window.mqc_highlight_f_texts.length - idx;
          }
        });
      }
      for (i = 0; i < ycats.length; i++) {
        $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
          if (f_text == "") {
            ycat_hl[i] = 0;
          } else if (
            (window.mqc_highlight_regex_mode && ycats[i].match(f_text)) ||
            (!window.mqc_highlight_regex_mode && ycats[i].indexOf(f_text) > -1)
          ) {
            ycat_hl[i] = window.mqc_highlight_f_texts.length - idx;
          }
        });
      }
      // Reshape the data - needs deepcopy as indexes are updated
      var newdata = JSON.parse(JSON.stringify(mqc_plots[target]["data"]));
      var new_xcats = [],
        new_ycats = [];
      var xidx = 0,
        yidx = 0;
      for (hl = window.mqc_highlight_f_texts.length; hl >= 0; hl--) {
        if (config["xcats_samples"]) {
          for (i = 0; i < xcats.length; i++) {
            if (xcat_hl[i] == hl) {
              new_xcats.push(xcats[i]);
              for (j = 0; j < data.length; j++) {
                // data[j] element is [x,y,val], get "x"
                if (data[j][0] == i) {
                  newdata[j][0] = xidx;
                }
              }
              xidx += 1;
            }
          }
        }
        if (config["ycats_samples"]) {
          for (i = 0; i < ycats.length; i++) {
            if (ycat_hl[i] == hl) {
              new_ycats.push(ycats[i]);
              for (j = 0; j < data.length; j++) {
                // data[j] element is [x,y,val], get "y"
                if (data[j][1] == i) {
                  newdata[j][1] = yidx;
                }
              }
              yidx += 1;
            }
          }
        }
      }
      data = newdata;
      if (config["xcats_samples"]) {
        xcats = new_xcats;
      }
      if (config["ycats_samples"]) {
        ycats = new_ycats;
      }
    }
  }

  // Hide samples
  var num_total = Math.max(xcats.length, ycats.length);
  $("#" + target)
    .closest(".hc-plot-wrapper")
    .parent()
    .find(".samples-hidden-warning")
    .remove();
  $("#" + target)
    .closest(".hc-plot-wrapper")
    .show();
  if (window.mqc_hide_f_texts.length > 0) {
    var remove = Array();
    if (config["xcats_samples"]) {
      var i = xcats.length;
      var xhidden = 0;
      // iterate over x-categories (columns)
      while (i--) {
        var match = false;
        for (j = 0; j < window.mqc_hide_f_texts.length; j++) {
          var f_text = window.mqc_hide_f_texts[j];
          if (window.mqc_hide_regex_mode) {
            if (xcats[i].match(f_text)) {
              match = true;
            }
          } else {
            if (xcats[i].indexOf(f_text) > -1) {
              match = true;
            }
          }
        }
        if (window.mqc_hide_mode == "show") {
          match = !match;
        }
        // modify data if "i" is match for "hiding":
        // mark elements from "i"-th column (x) for removal,
        // shift "x" of elements from "i+" columns to the left
        if (match) {
          xcats.splice(i, 1);
          for (n = 0; n < data.length; n++) {
            // data[n] element is [x,y,val], get "x"
            let x = data[n][0];
            if (x == i) {
              remove.push(n);
            } else if (x > i) {
              data[n][0] = x - 1;
            }
          }
          xhidden += 1;
        }
      }
    }
    if (config["ycats_samples"]) {
      var i = ycats.length;
      var yhidden = 0;
      // iterate over y-categories (rows)
      while (i--) {
        var match = false;
        for (j = 0; j < window.mqc_hide_f_texts.length; j++) {
          var f_text = window.mqc_hide_f_texts[j];
          if (window.mqc_hide_regex_mode) {
            if (ycats[i].match(f_text)) {
              match = true;
            }
          } else {
            if (ycats[i].indexOf(f_text) > -1) {
              match = true;
            }
          }
        }
        if (window.mqc_hide_mode == "show") {
          match = !match;
        }
        // modify data if "i" is match for "hiding"
        // mark elements from "i"-th row (y) for removal,
        // shift "y" of elements from "i+" rows up
        if (match) {
          ycats.splice(i, 1);
          for (n = 0; n < data.length; n++) {
            // data[n] element is [x,y,val], get "y"
            let y = data[n][1];
            if (y == i) {
              if (remove.indexOf(n) < 0) {
                remove.push(n);
              }
            } else if (y > i) {
              data[n][1] = y - 1;
            }
          }
          yhidden += 1;
        }
      }
    }
    // Remove the data values that matched
    remove = remove.sort(function (a, b) {
      return a - b;
    }); // Sorts alphabetically by default, even with integers
    var r = remove.length;
    while (r--) {
      data.splice(remove[r], 1);
    }
    // Report / hide the plot if we're hiding stuff
    var num_hidden = Math.max(xhidden, yhidden);
    // Some series hidden. Show a warning text string.
    if (num_hidden > 0) {
      var alert =
        '<div class="samples-hidden-warning alert alert-warning"><span class="glyphicon glyphicon-info-sign"></span> <strong>Warning:</strong> ' +
        num_hidden +
        ' samples hidden. <a href="#mqc_hidesamples" class="alert-link" onclick="mqc_toolbox_openclose(\'#mqc_hidesamples\', true); return false;">See toolbox.</a></div>';
      $("#" + target)
        .closest(".hc-plot-wrapper")
        .before(alert);
    }
    // All series hidden. Hide the graph.
    if (num_hidden >= num_total) {
      $("#" + target)
        .closest(".hc-plot-wrapper")
        .hide();
      return false;
    }
  }

  // Highlight samples - do this last as we convert numerical arrays to associative
  if (window.mqc_highlight_f_texts.length > 0) {
    $(".mqc_heatmap_sortHighlight").attr("disabled", false);
    var highlight_cells = Array();
    if (config["ycats_samples"]) {
      for (i = 0; i < xcats.length; i++) {
        $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
          if (f_text == "") {
            return true;
          }
          if (
            (window.mqc_highlight_regex_mode && xcats[i].match(f_text)) ||
            (!window.mqc_highlight_regex_mode && xcats[i].indexOf(f_text) > -1)
          ) {
            for (n = 0; n < data.length; n++) {
              highlight_cells[idx] =
                typeof highlight_cells[idx] != "undefined" && highlight_cells[idx] instanceof Array
                  ? highlight_cells[idx]
                  : [];
              // data[n] element is [x,y,val], get "x"
              if (data[n][0] == i) {
                highlight_cells[idx].push(n);
              }
            }
          }
        });
      }
    }
    if (config["ycats_samples"]) {
      for (i = 0; i < ycats.length; i++) {
        $.each(window.mqc_highlight_f_texts, function (idx, f_text) {
          if (f_text == "") {
            return true;
          }
          if (
            (window.mqc_highlight_regex_mode && ycats[i].match(f_text)) ||
            (!window.mqc_highlight_regex_mode && ycats[i].indexOf(f_text) > -1)
          ) {
            for (n = 0; n < data.length; n++) {
              // data[n] element is [x,y,val], get "y"
              if (data[n][1] == i) {
                highlight_cells[idx] =
                  typeof highlight_cells[idx] != "undefined" && highlight_cells[idx] instanceof Array
                    ? highlight_cells[idx]
                    : [];
                if (highlight_cells[idx].indexOf(n) < 0) {
                  highlight_cells[idx].push(n);
                }
              }
            }
          }
        });
      }
    }
    // Give highlighted cells a border
    for (var idx in highlight_cells) {
      var hl = highlight_cells[idx];
      hl = hl.sort(function (a, b) {
        return a - b;
      }); // Sorts alphabetically by default, even with integers
      var h = hl.length;
      while (h--) {
        var i = hl[h];
        data[i] = {
          // data[i] element is [x,y,val]
          x: data[i][0] === undefined ? data[i]["x"] : data[i][0],
          y: data[i][1] === undefined ? data[i]["y"] : data[i][1],
          value: data[i][2] === undefined ? data[i]["value"] : data[i][2],
          borderWidth: 2,
          borderColor: window.mqc_highlight_f_cols[idx],
        };
      }
    }
  } else {
    $(".mqc_heatmap_sortHighlight").attr("disabled", true);
  }

  // Build scale
  if (config["colstops"] === undefined) {
    config["colstops"] = [
      [0, "#313695"],
      [0.1, "#4575b4"],
      [0.2, "#74add1"],
      [0.3, "#abd9e9"],
      [0.4, "#e0f3f8"],
      [0.5, "#ffffbf"],
      [0.6, "#fee090"],
      [0.7, "#fdae61"],
      [0.8, "#f46d43"],
      [0.9, "#d73027"],
      [1, "#a50026"],
    ];
  }
  if (config["reverseColors"] === undefined) {
    config["reverseColors"] = false;
  }
  if (config["decimalPlaces"] === undefined) {
    config["decimalPlaces"] = 2;
  }
  if (config["legend"] === undefined) {
    config["legend"] = true;
  }
  if (config["borderWidth"] === undefined) {
    config["borderWidth"] = 0;
  }
  var datalabels = config["datalabels"];
  if (datalabels === undefined) {
    if (data.length < 20) {
      datalabels = true;
    } else {
      datalabels = false;
    }
  }
  // Clone the colstops before we mess around with them
  var colstops = JSON.parse(JSON.stringify(config["colstops"]));
  // Reverse the colour scale if the axis is reversed
  if (config["reverseColors"]) {
    for (var i = 0; i < colstops.length; i++) {
      colstops[i][0] = 1 - colstops[i][0];
    }
    colstops.reverse();
  }

  // Make the highcharts plot
  Highcharts.chart(
    target,
    {
      chart: {
        type: "heatmap",
        zoomType: "xy",
        height: config["square"] ? 500 : undefined,
        width: config["square"] ? 530 : undefined,
        marginTop: config["title"] ? 60 : 50,
      },
      plotOptions: {
        series: {
          states: {
            hover: {
              borderWidth: 2,
              borderColor: "red",
            },
          },
        },
      },
      title: {
        text: config["title"],
      },
      xAxis: {
        endOnTick: false,
        maxPadding: 0,
        categories: xcats,
        title: { enabled: true, text: config["xTitle"] },
        labels: {
          formatter: function () {
            try {
              return this.value.substr(0, 20);
            } catch (err) {
              return this.value;
            }
          },
        },
      },
      yAxis: {
        endOnTick: false,
        maxPadding: 0,
        categories: ycats,
        reversed: true,
        opposite: true,
        title: config["yTitle"],
        labels: {
          formatter: function () {
            try {
              return this.value.substr(0, 20);
            } catch (err) {
              return this.value;
            }
          },
        },
      },
      colorAxis: {
        reversed: config["reverseColors"],
        stops: colstops,
        min: config["min"],
        max: config["max"],
      },
      legend: {
        align: "right",
        layout: "vertical",
        margin: 0,
        verticalAlign: "top",
        y: 25,
        symbolHeight: 280,
        enabled: config["legend"],
      },
      tooltip: {
        useHTML: true,
        formatter: function () {
          return (
            'X: <span style="font-weight:bold; font-family:monospace;">' +
            this.series.xAxis.categories[this.point.x] +
            "</span><br>" +
            'Y: <span style="font-weight:bold; font-family:monospace;">' +
            this.series.yAxis.categories[this.point.y] +
            "</span><br>" +
            '<div style="background-color:' +
            this.point.color +
            '; display:inline-block; height: 10px; width: 10px; border:1px solid #333;"></div> ' +
            '<span style="font-weight: bold; text-decoration:underline;">' +
            Highcharts.numberFormat(this.point.value, config["decimalPlaces"]) +
            "</span>"
          );
        },
      },
      series: [
        {
          turboThreshold: 0,
          borderWidth: config["borderWidth"],
          data: data,
          dataLabels: {
            enabled: datalabels,
            format: "{point.value:." + config["decimalPlaces"] + "f}",
            color: config["datalabel_colour"],
          },
        },
      ],
    },
    function (this_chart) {
      // Maintain aspect ratio as chart size changes
      if (config["square"]) {
        var resizeCh = function (chart) {
          // Extra width for legend
          var lWidth = chart.options.legend.enabled ? 30 : 0;
          // Work out new chart width, assuming needs to be narrower
          var chHeight = $(chart.renderTo).height();
          var chWidth = $(chart.renderTo).width();
          var nChHeight = chHeight;
          var nChWidth = chHeight + lWidth;
          // Chart is already too narrow, make it less tall
          if (chWidth < nChWidth) {
            nChHeight = chWidth - lWidth;
            nChWidth = chWidth;
          }
          chart.setSize(nChWidth, nChHeight);
        };
        // Resize on load
        resizeCh(this_chart);
        // Resize on graph resize
        $(this_chart.renderTo).on("mqc_plotresize", function (e) {
          try {
            resizeCh(this_chart);
          } catch (e) {
            plot_heatmap($(this).attr("id"));
          }
        });
      }
    },
  );

  // Listeners for range slider
  $(".mqc_hcplot_range_sliders input").on("keyup change input", function () {
    target = $(this).data("target");
    minmax = $(this).data("minmax");
    var chart = $("#" + target).highcharts();
    if (minmax == "min") {
      chart.colorAxis[0].update({ min: $(this).val() });
    }
    if (minmax == "max") {
      chart.colorAxis[0].update({ max: $(this).val() });
    }
    $("#" + target + "_range_slider_" + minmax + ", #" + target + "_range_slider_" + minmax + "_txt").val(
      $(this).val(),
    );
  });
}

// Highlight text with a fadeout background colour highlight
function highlight_fade_text(obj) {
  var orig_col = $(obj).css("color");
  obj.css({
    display: "inline-block",
    "background-color": "#5bc0de",
    color: "#FFFFFF",
    WebkitTransition: "background-color 0s, color 0s",
    MozTransition: "background-color 0s, color 0s",
    MsTransition: "background-color 0s, color 0s",
    OTransition: "background-color 0s, color 0s",
    transition: "background-color 0s, color 0s",
  });
  setTimeout(function () {
    obj.css({
      "background-color": "#FFFFFF",
      color: orig_col,
      WebkitTransition: "background-color 0.5s, color 0.5s",
      MozTransition: "background-color 0.5s, color 0.5s",
      MsTransition: "background-color 0.5s, color 0.5s",
      OTransition: "background-color 0.5s, color 0.5s",
      transition: "background-color 0.5s, color 0.5s",
    });
  }, 500);
}
